﻿// Parts Copyright 2008 Newgrove Consultants Limited
// Modified By John Diss 
// From original work:
//
// Copyright 2006 - Ricardo Stuven (rstuven@gmail.com)
//
// This file is part of MsSqlSpatial.
// MsSqlSpatial is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
// 
// MsSqlSpatial is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.

// You should have received a copy of the GNU Lesser General Public License
// along with MsSqlSpatial; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

using System.Collections.Generic;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using GeoAPI.CoordinateSystems;
using GeoAPI.CoordinateSystems.Transformations;
using GeoAPI.Geometries;
using Microsoft.SqlServer.Server;
using MsSqlSpatial;

public partial class UserDefinedFunctions
{
    /// <summary>
    /// To Do: remove dependency on MsSqlCoordinate2D if CoordinateTransformationFactory becomes abstract
    /// </summary>
    /// <param name="ewkb"></param>
    /// <param name="target_srid"></param>
    /// <returns></returns>
    [SqlFunction(IsDeterministic = false, DataAccess = DataAccessKind.Read)]
    public static SqlBytes Transform(SqlBytes ewkb, SqlInt32 target_srid)
    {
        IGeometry geometry = ConvertGeometry.ToGeometry(ewkb);
        if (geometry != null)
        {
            int sourceSRID = GeometryUtilities.MsSqlSpatialSrid(geometry);
            int targetSRID = !target_srid.IsNull ? target_srid.Value : -1;

            // Return the original geometry if SRIDs are equal
            if (sourceSRID == targetSRID)
                return ewkb;
            else
            {
                // Look up coordinate system transformation object in cache
                var cacheKey = new TransformCacheKey(sourceSRID, targetSRID);
                IMathTransform mathTransform;

                if (!Global
                   .Services
                   .TransformCache
                   .TryGetValue(cacheKey, out mathTransform))
                {
                    if (!SpatialRefSysTable.IsValidSRID(targetSRID))
                        throw new InvalidSridException(targetSRID);

                    // Create coordinate system transformation object and put it in cache
                    string sourceSRWKT = SpatialRefSysTable.GetSRText(sourceSRID);
                    string targetSRWKT = SpatialRefSysTable.GetSRText(targetSRID);

                    ICoordinateSystem sourceCS =
                        Global
                            .Services
                            .CoordinateSystemFactory.
                            CreateFromWkt(sourceSRWKT);

                    ICoordinateSystem targetCS =
                        Global
                            .Services
                            .CoordinateSystemFactory
                            .CreateFromWkt(targetSRWKT);

                    ICoordinateTransformation transform =
                        Global
                            .Services
                            .CoordinateTransformationFactory
                            .CreateFromCoordinateSystems(sourceCS, targetCS);
                    lock (Global.Services.TransformCache)
                    {
                        if (!Global
                                 .Services
                                 .TransformCache
                                 .TryGetValue(cacheKey, out mathTransform))
                        {
                            mathTransform = transform.MathTransform;

                        }
                    }

                }
                // Transform source geometry's coordinate system
                IGeometry sourceGeometry = geometry;
                IGeometry targetGeometry =
                    GeometryTransform.TransformGeometry(
                        sourceGeometry,
                        mathTransform,
                        Global
                            .Services.SridAwareGeometryFactory(targetSRID));

                return ConvertGeometry.ToSqlBytes(targetGeometry, targetSRID);
            }
        }
        return SqlBytes.Null;
    }

    // This trigger is registered in SpatialRefSysTable::ExecuteCreate
    public static void RefreshTransformCacheTrigger()
    {
        SqlTriggerContext triggerContext = SqlContext.TriggerContext;
        using (SqlConnection connection = DatabaseHelper.ContextConnection)
        {
            connection.Open();
            using (SqlCommand command = connection.CreateCommand())
            {
                // Get SRID of updated SRTEXT or deleted record.
                int srid = -1;
                if (triggerContext.TriggerAction == TriggerAction.Update
                    && triggerContext.IsUpdatedColumn(3 /* SRTEXT */))
                {
                    DatabaseHelper.BuildCommand(command, "SELECT srid FROM inserted");
                    try
                    {
                        srid = (int)command.ExecuteScalar();
                    }
                    catch
                    {
                    }
                }
                else if (triggerContext.TriggerAction == TriggerAction.Delete)
                {
                    DatabaseHelper.BuildCommand(command, "SELECT srid FROM deleted");
                    try
                    {
                        srid = (int)command.ExecuteScalar();
                    }
                    catch
                    {
                    }
                }
#if DEBUG
                SqlContext.Pipe.Send(string.Format("Trigger on SRID: {0}", srid));
#endif
                if (srid != -1)
                {
                    // Build list of cache key items containing SRID
                    var removeKeys = new List<TransformCacheKey>();
                    foreach (TransformCacheKey cacheKey in Global.Services.TransformCache.Keys)
                    {
                        if (cacheKey.part1 == srid || cacheKey.part2 == srid)
                            removeKeys.Add(cacheKey);
                    }
                    // Remove by found cache keys
                    foreach (TransformCacheKey cacheKey in removeKeys)
                    {
                        Global.Services.TransformCache.Remove(cacheKey);
#if DEBUG
                        SqlContext.Pipe.Send(string.Format("{0} removed from transform cache.", cacheKey));
#endif
                    }
                }
            }
        }
    }

#if DEBUG
    [SqlProcedure]
    public static void TransformCache_List()
    {
        SqlContext.Pipe.Send(string.Format("{0} items in cache.", Global.Services.TransformCache.Count));
        foreach (var item in Global.Services.TransformCache)
            SqlContext.Pipe.Send(string.Format("\t{0}", item.Key));
    }

    [SqlProcedure]
    public static void TransformCache_Clear()
    {
        Global.Services.TransformCache.Clear();
        SqlContext.Pipe.Send("Cache cleared.");
    }

    [SqlProcedure]
    public static void TransformCache_Get(int sourceSRID, int targetSRID)
    {
        var key = new TransformCacheKey(sourceSRID, targetSRID);
        IMathTransform mathTransform;
        if (Global.Services.TransformCache.TryGetValue(key, out mathTransform))
            SqlContext.Pipe.Send(string.Format("{0} exists in cache.", key));
        else
            SqlContext.Pipe.Send(string.Format("{0} doesn't exist in cache.", key));
    }

    [SqlProcedure]
    public static void TransformCache_Remove(int sourceSRID, int targetSRID)
    {
        var key = new TransformCacheKey(sourceSRID, targetSRID);
        if (Global.Services.TransformCache.Remove(key))
            SqlContext.Pipe.Send(string.Format("{0} removed from cache.", key));
        else
            SqlContext.Pipe.Send(string.Format("{0} doesn't exist in cache.", key));
    }
#endif
} ;

public class TransformCacheKey : CompositeKey<int, int>, IEqualityComparer<TransformCacheKey>
{
    public TransformCacheKey(int sourceSRID, int targetSRID)
        : base(sourceSRID, targetSRID)
    {
    }

    #region IEqualityComparer<TransformCacheKey> Members

    public bool Equals(TransformCacheKey x, TransformCacheKey y)
    {
        return base.Equals(x, y);
    }

    public int GetHashCode(TransformCacheKey obj)
    {
        return base.GetHashCode(obj);
    }

    #endregion
}

public class CompositeKey<TPart1, TPart2> : IEqualityComparer<CompositeKey<TPart1, TPart2>>
{
    /// <summary>
    /// made public to allow testing
    /// </summary>
    public readonly TPart1 part1;

    public readonly TPart2 part2;

   
    public CompositeKey(TPart1 part1, TPart2 part2)
    {
        this.part1 = part1;
        this.part2 = part2;
    }

    #region IEqualityComparer<CompositeKey<TPart1,TPart2>> Members

    public bool Equals(CompositeKey<TPart1, TPart2> x, CompositeKey<TPart1, TPart2> y)
    {
        return x.part1.Equals(y.part1) && x.part2.Equals(y.part2);
    }

    public int GetHashCode(CompositeKey<TPart1, TPart2> obj)
    {
        return obj.part1.GetHashCode() ^ obj.part2.GetHashCode();
    }

    #endregion

#if DEBUG
    public override string ToString()
    {
        return string.Format("({0}, {1})", part1, part2);
    }
#endif
}